/**
 * Copyright 2010-present Facebook.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook;

import android.os.Bundle;
import com.facebook.internal.Validate;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * <p>
 * A base class for implementations of a {@link Session Session} token cache.
 * </p>
 * <p>
 * The Session constructor optionally takes a TokenCachingStrategy, from which it will
 * attempt to load a cached token during construction. Also, whenever the
 * Session updates its token, it will also save the token and associated state
 * to the TokenCachingStrategy.
 * </p>
 * <p>
 * This is the only mechanism supported for an Android service to use Session.
 * The service can create a custom TokenCachingStrategy that returns the Session provided
 * by an Activity through which the user logged in to Facebook.
 * </p>
 */
public abstract class TokenCachingStrategy
{
  /**
   * The key used by Session to store the token value in the Bundle during
   * load and save.
   */
  public static final String TOKEN_KEY = "com.facebook.TokenCachingStrategy.Token";

  /**
   * The key used by Session to store the expiration date value in the Bundle
   * during load and save.
   */
  public static final String EXPIRATION_DATE_KEY = "com.facebook.TokenCachingStrategy.ExpirationDate";

  /**
   * The key used by Session to store the last refresh date value in the
   * Bundle during load and save.
   */
  public static final String LAST_REFRESH_DATE_KEY = "com.facebook.TokenCachingStrategy.LastRefreshDate";

  /**
   * The key used by Session to store the user's id value in the Bundle during
   * load and save.
   */
  public static final String USER_FBID_KEY = "com.facebook.TokenCachingStrategy.UserFBID";

  /**
   * The key used by Session to store an enum indicating the source of the token
   * in the Bundle during load and save.
   */
  public static final String TOKEN_SOURCE_KEY = "com.facebook.TokenCachingStrategy.AccessTokenSource";

  /**
   * The key used by Session to store the list of permissions granted by the
   * token in the Bundle during load and save.
   */
  public static final String PERMISSIONS_KEY = "com.facebook.TokenCachingStrategy.Permissions";

  private static final long INVALID_BUNDLE_MILLISECONDS = Long.MIN_VALUE;
  private static final String IS_SSO_KEY = "com.facebook.TokenCachingStrategy.IsSSO";

  /**
   * Called during Session construction to get the token state. Typically this
   * is loaded from a persistent store that was previously initialized via
   * save.  The caller may choose to keep a reference to the returned Bundle
   * indefinitely.  Therefore the TokenCachingStrategy should not store the returned Bundle
   * and should return a new Bundle on every call to this method.
   *
   * @return A Bundle that represents the token state that was loaded.
   */
  public abstract Bundle load();

  /**
   * Called when a Session updates its token. This is passed a Bundle of
   * values that should be stored durably for the purpose of being returned
   * from a later call to load.  Some implementations may choose to store
   * bundle beyond the scope of this call, so the caller should keep no
   * references to the bundle to ensure that it is not modified later.
   *
   * @param bundle A Bundle that represents the token state to be saved.
   */
  public abstract void save(Bundle bundle);

  /**
   * Called when a Session learns its token is no longer valid or during a
   * call to {@link Session#closeAndClearTokenInformation
   * closeAndClearTokenInformation} to clear the durable state associated with
   * the token.
   */
  public abstract void clear();

  /**
   * Returns a boolean indicating whether a Bundle contains properties that
   * could be a valid saved token.
   *
   * @param bundle A Bundle to check for token information.
   * @return a boolean indicating whether a Bundle contains properties that
   * could be a valid saved token.
   */
  public static boolean hasTokenInformation(Bundle bundle)
  {
    if (bundle == null)
    {
      return false;
    }

    String token = bundle.getString(TOKEN_KEY);
    if ((token == null) || (token.length() == 0))
    {
      return false;
    }

    long expiresMilliseconds = bundle.getLong(EXPIRATION_DATE_KEY, 0L);
    if (expiresMilliseconds == 0L)
    {
      return false;
    }

    return true;
  }

  /**
   * Gets the cached token value from a Bundle.
   *
   * @param bundle A Bundle in which the token value was stored.
   * @return the cached token value, or null.
   * @throws NullPointerException if the passed in Bundle is null
   */
  public static String getToken(Bundle bundle)
  {
    Validate.notNull(bundle, "bundle");
    return bundle.getString(TOKEN_KEY);
  }

  /**
   * Puts the token value into a Bundle.
   *
   * @param bundle A Bundle in which the token value should be stored.
   * @param value  The String representing the token value, or null.
   * @throws NullPointerException if the passed in Bundle or token value are null
   */
  public static void putToken(Bundle bundle, String value)
  {
    Validate.notNull(bundle, "bundle");
    Validate.notNull(value, "value");
    bundle.putString(TOKEN_KEY, value);
  }

  /**
   * Gets the cached expiration date from a Bundle.
   *
   * @param bundle A Bundle in which the expiration date was stored.
   * @return the cached expiration date, or null.
   * @throws NullPointerException if the passed in Bundle is null
   */
  public static Date getExpirationDate(Bundle bundle)
  {
    Validate.notNull(bundle, "bundle");
    return getDate(bundle, EXPIRATION_DATE_KEY);
  }

  /**
   * Puts the expiration date into a Bundle.
   *
   * @param bundle A Bundle in which the expiration date should be stored.
   * @param value  The Date representing the expiration date.
   * @throws NullPointerException if the passed in Bundle or date value are null
   */
  public static void putExpirationDate(Bundle bundle, Date value)
  {
    Validate.notNull(bundle, "bundle");
    Validate.notNull(value, "value");
    putDate(bundle, EXPIRATION_DATE_KEY, value);
  }

  /**
   * Gets the cached expiration date from a Bundle.
   *
   * @param bundle A Bundle in which the expiration date was stored.
   * @return the long representing the cached expiration date in milliseconds
   * since the epoch, or 0.
   * @throws NullPointerException if the passed in Bundle is null
   */
  public static long getExpirationMilliseconds(Bundle bundle)
  {
    Validate.notNull(bundle, "bundle");
    return bundle.getLong(EXPIRATION_DATE_KEY);
  }

  /**
   * Puts the expiration date into a Bundle.
   *
   * @param bundle A Bundle in which the expiration date should be stored.
   * @param value  The long representing the expiration date in milliseconds
   *               since the epoch.
   * @throws NullPointerException if the passed in Bundle is null
   */
  public static void putExpirationMilliseconds(Bundle bundle, long value)
  {
    Validate.notNull(bundle, "bundle");
    bundle.putLong(EXPIRATION_DATE_KEY, value);
  }

  /**
   * Gets the cached list of permissions from a Bundle.
   *
   * @param bundle A Bundle in which the list of permissions was stored.
   * @return the cached list of permissions.
   * @throws NullPointerException if the passed in Bundle is null
   */
  public static List<String> getPermissions(Bundle bundle)
  {
    Validate.notNull(bundle, "bundle");
    return bundle.getStringArrayList(PERMISSIONS_KEY);
  }

  /**
   * Puts the list of permissions into a Bundle.
   *
   * @param bundle A Bundle in which the list of permissions should be stored.
   * @param value  The List&lt;String&gt; representing the list of permissions,
   *               or null.
   * @throws NullPointerException if the passed in Bundle or permissions list are null
   */
  public static void putPermissions(Bundle bundle, List<String> value)
  {
    Validate.notNull(bundle, "bundle");
    Validate.notNull(value, "value");

    ArrayList<String> arrayList;
    if (value instanceof ArrayList<?>)
    {
      arrayList = (ArrayList<String>) value;
    }
    else
    {
      arrayList = new ArrayList<String>(value);
    }
    bundle.putStringArrayList(PERMISSIONS_KEY, arrayList);
  }

  /**
   * Gets the cached enum indicating the source of the token from the Bundle.
   *
   * @param bundle A Bundle in which the enum was stored.
   * @return enum indicating the source of the token
   * @throws NullPointerException if the passed in Bundle is null
   */
  public static AccessTokenSource getSource(Bundle bundle)
  {
    Validate.notNull(bundle, "bundle");
    if (bundle.containsKey(TokenCachingStrategy.TOKEN_SOURCE_KEY))
    {
      return (AccessTokenSource) bundle.getSerializable(TokenCachingStrategy.TOKEN_SOURCE_KEY);
    }
    else
    {
      boolean isSSO = bundle.getBoolean(TokenCachingStrategy.IS_SSO_KEY);
      return isSSO ? AccessTokenSource.FACEBOOK_APPLICATION_WEB : AccessTokenSource.WEB_VIEW;
    }
  }

  /**
   * Puts the enum indicating the source of the token into a Bundle.
   *
   * @param bundle A Bundle in which the enum should be stored.
   * @param value  enum indicating the source of the token
   * @throws NullPointerException if the passed in Bundle is null
   */
  public static void putSource(Bundle bundle, AccessTokenSource value)
  {
    Validate.notNull(bundle, "bundle");
    bundle.putSerializable(TOKEN_SOURCE_KEY, value);
  }

  /**
   * Gets the cached last refresh date from a Bundle.
   *
   * @param bundle A Bundle in which the last refresh date was stored.
   * @return the cached last refresh Date, or null.
   * @throws NullPointerException if the passed in Bundle is null
   */
  public static Date getLastRefreshDate(Bundle bundle)
  {
    Validate.notNull(bundle, "bundle");
    return getDate(bundle, LAST_REFRESH_DATE_KEY);
  }

  /**
   * Puts the last refresh date into a Bundle.
   *
   * @param bundle A Bundle in which the last refresh date should be stored.
   * @param value  The Date representing the last refresh date, or null.
   * @throws NullPointerException if the passed in Bundle or date value are null
   */
  public static void putLastRefreshDate(Bundle bundle, Date value)
  {
    Validate.notNull(bundle, "bundle");
    Validate.notNull(value, "value");
    putDate(bundle, LAST_REFRESH_DATE_KEY, value);
  }

  /**
   * Gets the cached last refresh date from a Bundle.
   *
   * @param bundle A Bundle in which the last refresh date was stored.
   * @return the cached last refresh date in milliseconds since the epoch.
   * @throws NullPointerException if the passed in Bundle is null
   */
  public static long getLastRefreshMilliseconds(Bundle bundle)
  {
    Validate.notNull(bundle, "bundle");
    return bundle.getLong(LAST_REFRESH_DATE_KEY);
  }

  /**
   * Puts the last refresh date into a Bundle.
   *
   * @param bundle A Bundle in which the last refresh date should be stored.
   * @param value  The long representing the last refresh date in milliseconds
   *               since the epoch.
   * @throws NullPointerException if the passed in Bundle is null
   */
  public static void putLastRefreshMilliseconds(Bundle bundle, long value)
  {
    Validate.notNull(bundle, "bundle");
    bundle.putLong(LAST_REFRESH_DATE_KEY, value);
  }

  static Date getDate(Bundle bundle, String key)
  {
    if (bundle == null)
    {
      return null;
    }

    long n = bundle.getLong(key, INVALID_BUNDLE_MILLISECONDS);
    if (n == INVALID_BUNDLE_MILLISECONDS)
    {
      return null;
    }

    return new Date(n);
  }

  static void putDate(Bundle bundle, String key, Date date)
  {
    bundle.putLong(key, date.getTime());
  }
}
